package com.ease.util {
   import flash.display.DisplayObject;
   import flash.display.DisplayObjectContainer;
   import flash.geom.Point;
   import flash.utils.getQualifiedClassName;

   import mx.binding.utils.BindingUtils;
   import mx.core.IChildList;
   import mx.managers.ISystemManager;

   public class ComponentUtil {
      /**
       * Constructs a new <code>ComponentUtil</code> instance.
       */
      public function ComponentUtil() {
         super();
         throw new Error("Cannot instantiate class '" + getQualifiedClassName(this) + "'.");
      }

      /**
       * Determines whether a global <code>Point</code> is within the area of
       * a given <code>DisplayObject</code>.
       *
       * @param object The <code>DisplayObject</code> to check to see if it is
       *            drawn over the area containing
       *            <code>globalPoint</code>.
       * @param globalPoint A global <code>Point</code> to check for within
       *               the bounds of <code>object</code>.
       *
       * @return <code>true</code> if <code>globalPoint</code> is within the
       *       area rendered by <code>object</code>; otherwise,
       *       <code>false</code>.
       */
      public static function containsPoint(object:DisplayObject, globalPoint:Point):Boolean {
         if (!object) {
            throw new Error("Argument 'object' cannot be null.");
         }
         if (!globalPoint) {
            throw new Error("Argument 'globalPoint' cannot be null.");
         }
         var localPoint:Point = object.globalToLocal(globalPoint);
         return localPoint.x >= 0 && localPoint.y >= 0 && localPoint.x < object.width && localPoint.y < object.height;
      }

      /**
       * Recursively searches the tree of <code>DisplayObject</code>s, as with
       * <code>findObjectUnderPoint</code>, using the current global position
       * of the mouse as the target point.
       *
       * @param root The root <code>DisplayObject</code> under which to
       *          search for the object targetted by the point.
       *
       * @return The object under <code>root</code> which is targetted by the
       *       point. If none is targetted, returns <code>null</code>.
       */
      public static function findObjectUnderMouse(root:DisplayObject):DisplayObject {
         if (!root) {
            throw new Error("Argument 'root' cannot be null.");
         }
         var localMousePoint:Point = new Point(root.mouseX, root.mouseY);
         return findObjectUnderPoint(root, root.localToGlobal(localMousePoint));
      }

      /**
       * Recursively searches the tree of <code>DisplayObject</code>s to find
       * the object targetted by a given global point.
       *
       * <p>
       * Flash lays out objects with a higher Z level above others in the same
       * <code>DisplayObjectContainer</code>, and when the Z level is equal
       * lays out the later children in the child list above ones earlier in
       * the list.
       * </p>
       *
       * <p>
       * With this in mind, this algorithm will accept a
       * <code>DisplayObject</code> as input. If that object is a container,
       * it will recursively call itself passing in each child, starting with
       * the end of the child list. If the algorithm finds a match for a child,
       * the child is remembered along with the match. Once a child is found
       * with a match, no other children in the same container with equal or
       * lower Z levels are searched. Thus, the child laid out above all
       * others under the mouse coordinatese will be returned.
       * </p>
       *
       * <p>
       * If no child with a match is found, or if the algorithm is running on
       * a leaf object, it will check if the local mouse coordinates are
       * within the shape. If they are, the algorithm returns the object.
       * </p>
       *
       * @param root The root <code>DisplayObject</code> under which to
       *          search for the object targetted by the point.
       * @param globalPoint The global point (the point relative to the
       *               upper-left corner of the application) under which
       *               to find the targetted object.
       *
       * @return The object under <code>root</code> which is targetted by the
       *       point. If none is targetted, returns <code>null</code>.
       */
      public static function findObjectUnderPoint(root:DisplayObject, globalPoint:Point):DisplayObject {
         if (!root) {
            throw new Error("Argument 'root' cannot be null.");
         }
         if (!globalPoint) {
            throw new Error("Argument 'globalPoint' cannot be null.");
         }
         var foundObject:DisplayObject;
         var foundObjectAncestor:DisplayObject;
         if (isVisible(root) && containsPoint(root, globalPoint)) {
            if (root is DisplayObjectContainer) {
               var container:DisplayObjectContainer = DisplayObjectContainer(root);
               var childList:Array = getChildren(container);
               for (var i:int = childList.length - 1; i >= 0; i--) {
                  var child:DisplayObject = childList[i] as DisplayObject;
                  if (!foundObjectAncestor || (getZIndex(child) > getZIndex(foundObjectAncestor))) {
                     var tentativeFoundObject:DisplayObject = findObjectUnderPoint(child, globalPoint);
                     if (tentativeFoundObject) {
                        foundObject = tentativeFoundObject;
                        foundObjectAncestor = child;
                     }
                  }
               }
            }
            return foundObject ? foundObject : root;
         } else {
            return null;
         }
      }

      /**
       * Returns an array of the child <code>DisplayObject</code>s of a given
       * <code>DisplayObjectContainer</code>. The child list includes children
       * which are pop-ups added with the
       * <code>PopUpChildManagerList.POPUP</code> mode.
       *
       * @param container The <code>DisplayObjectContainer</code> whose children
       *                  should be returned.
       *
       * @return An <code>Array</code> of <code>DisplayObject</code>s which are
       *         children of <code>container</code>.
       */
      public static function getChildren(container:DisplayObjectContainer):Array {
         var children:Array = new Array(container.numChildren);
         for (var i:int = 0; i < container.numChildren; i++) {
            children[i] = container.getChildAt(i);
         }
         if (container is ISystemManager) {
            var childList:IChildList = ISystemManager(container).popUpChildren;
            for (i = 0; i < childList.numChildren; i++) {
               children.push(childList.getChildAt(i));
            }
         }
         return children;
      }

      /**
       * Returns the <code>z</code> property of the given <code>Object</code>,
       * if it is accessible. If there is no <code>z</code> property 0 will be
       * returned. This function enables the <code>z</code> property to be
       * handled in Flash Player 10 in a way that is backward compatible with
       * Flash Player 9.
       *
       * @param object The <code>DisplayObject</code> whose <code>z</code>
       *            property should be returned. This is typed as
       *            <code>Object</code> so that there is no compiler check
       *            on accessing the <code>z</code> property, enabling this
       *            function to work properly with both Flash Player 9 and
       *            10.
       *
       * @return The value of the <code>z</code> property, or 0 if
       *       <code>object</code> has no <code>z</code> property.
       */
      private static function getZIndex(object:Object):int {
         var z:int;
         try {
            z = object.z;
         } catch (e:ReferenceError) {
            z = 0;
         }
         return z;
      }

      /**
       * Determines whether a <code>DisplayObject</code> is visible.
       *
       * @param object The <code>DisplayObject</code> whose visibility is to
       *            be determined.
       *
       * @return <code>true</code> if <code>object</code> is visible;
       *       otherwise, <code>false</code>.
       */
      public static function isVisible(object:DisplayObject):Boolean {
         if (!object) {
            throw new Error("Argument 'object' cannot be null.");
         }
         return object.visible && object.width > 0 && object.height > 0;
      }

      /**
       * Creates bi-directional binding between widget and data.
       * @param widget
       *    Widget, e.g. TextInput
       * @param widgetProp
       *    Widget's property to bind
       * @param data
       *    Data structure (model)
       * @param dataProp
       *    property of data to bind
       */
      public static function biBind(widget:Object, wprop:String, data:Object, dprop:String):void {
         BindingUtils.bindProperty(widget, wprop, data, dprop);
         BindingUtils.bindProperty(data, dprop, widget, wprop);
      }

      /**
       * Returns container if it is of the given class, else the first nested component
       * of the given class, else null.
       *
       * @param container
       *    The container in which to look for a component of class c (the container
       *    itself is a match candidate)
       *
       * @param c
       *    The class to look for
       *
       * @return the first component found to be of class c, or null if no such component
       *    is found
       */
      public static function getComponentByClass(container:DisplayObjectContainer, c:Class):* {
         if (container == null) {
            return null;
         }
         if (container is c) {
            return container;
         }
         for (var i:int; i < container.numChildren; ++i) {
            var child:DisplayObject = container.getChildAt(i);
            if (child is c) {
               return child;
            }

            if (child is DisplayObjectContainer) {
               var nestedchild:DisplayObject = getComponentByClass(DisplayObjectContainer(child), c);
               if (nestedchild != null) {
                  return nestedchild;
               }
            }
         }

         return null;
      }

      /**
       * Calls the given function on the given component and its children,
       * recursively.
       *
       * @param component
       *    The root component on which to call the function.
       *
       * @param action
       *    <code>function(component:DisplayObject):void</code>.
       *
       * @param skipFlexTypes
       *    Whether to skip the Flex sdk types.
       */
      public static function forEachComponentDeep(component:DisplayObject,
                                                  action:Function,  skipFlexTypes:Boolean = false):void {
         if (!skipFlexTypes || !ReflectionUtil.isFlexType(component)) {
            action(component);
         }
         var container:DisplayObjectContainer = component as DisplayObjectContainer;
         if (container == null) {
            return;
         }
         for (var i:int = 0, n:int = container.numChildren; i < n; ++i) {
            forEachComponentDeep(container.getChildAt(i), action, skipFlexTypes);
         }
      }
   }
}
